#!/usr/bin/env python
# -*- coding: utf-8 -*-
# FTPd python simple release 

HOST       = '192.168.1.54'
PORT       = 1026
DATA_PORTS = range( 50000, 55000 )
MAX_CLIENT = 15
CLIENT_CONTEXT = {'user':None, 'auth':False, 'pwd':[], 'mode':'A',\
'datasock':None, 'dataport':None}
FTPROOT = ['home', 'joe']

import os
import sys
import socket
import thread

def send_reply( cliSock, code, lines=[] ):
  """
Code	Description
110	Restart marker reply.
120	Service ready in nnn minutes.
125	Data connection already open; transfer starting.
150	File status okay; about to open data connection.
200	Command okay.
202	Command not implemented, superfluous at this site.
211	System status, or system help reply.
212	Directory status.
213	File status.
214	Help message.
215	NAME system type.
220	Service ready for new user.
221	Service closing control connection.
225	Data connection open; no transfer in progress.
226	Closing data connection.
227	Entering Passive Mode <h1,h2,h3,h4,p1,p2>.
228	Entering Long Passive Mode.
229	Extended Passive Mode Entered.
230	User logged in, proceed.
250	Requested file action okay, completed.
257	"PATHNAME" created.
331	User name okay, need password.
332	Need account for login.
350	Requested file action pending further information.
421 	Service not available, closing control connection.
425 	Can't open data connection.
426 	Connection closed; transfer aborted.
450 	Requested file action not taken.
451 	Requested action aborted. Local error in processing.
452 	Requested action not taken.
500	Syntax error, command unrecognized.
501	Syntax error in parameters or arguments.
502	Command not implemented.
503	Bad sequence of commands.
504	Command not implemented for that parameter.
521	Supported address families are <af1, .., afn>
522	Protocol not supported.
530	Not logged in.
532	Need account for storing files.
550	Requested action not taken.
551	Requested action aborted. Page type unknown.
552	Requested file action aborted.
553	Requested action not taken.
554	Requested action not taken: invalid REST parameter.
555	Requested action not taken: type or stru mismatch.
  """
  for i in lines:
    try:
      cliSock.send( '%d %s\r\n' % ( code, i ) )
    except:
      sys.stderr.write( 'send_reply(): failed\n' )
      sys.exit( 1 )

    sys.stderr.write( 'SERVER: %d %s\n' % ( code, i ) )

def stripNL( data ):
  if   data.endswith( '\r\n' ):
    return data[:-2]

  elif data.endswith( '\r' ) or data.endswith( '\n' ):
    return data[:-1]

  else:
    return data

# void
def init_new_client( cliSock, cliAddr ):
  send_reply( cliSock, 220, ['Welcome'] )

def mkDataConn( ):
  cs = socket.socket( socket.AF_INET, socket.SOCK_STREAM )

  # search for first free port to bind to
  for port in DATA_PORTS:
    try:
      cs.bind( ( HOST, port ) )
    except:
      continue
    else:
      break

  cs.listen( 1 )
  strPort = ','.join( HOST.split('.') ) + ',' + \
    str( port>>8 ) + ',' + str( port&255 )

  return cs, strPort

def cleanUpDataSock( CLIENT_CONTEXT ):
  CLIENT_CONTEXT['datasock'].shutdown( 2 )
  CLIENT_CONTEXT['datasock'].close( )
  CLIENT_CONTEXT['dataport'] = None


def parse_request( cliSock, cmd, args ):

  sys.stderr.write( 'CLIENT: %s %s\n' % ( cmd, args ) )
  sys.stderr.flush( )

  if   cmd == "ABOR":	#Abort.
    send_reply( cliSock, 202, ['Command not implemented'] )

  elif cmd == "ACCT":	#Account.
    send_reply( cliSock, 202, ['Command not implemented'] )

  elif cmd == "ADAT":	#Authentication/Security Data.	RFC 2228
    send_reply( cliSock, 202, ['Command not implemented'] )

  elif cmd == "ALLO":	#Allocate.
    send_reply( cliSock, 202, ['Command not implemented'] )

  elif cmd == "APPE":	#Append.
    send_reply( cliSock, 202, ['Command not implemented'] )

  elif cmd == "AUTH":	#Authentication/Security Mechanism.	RFC 2228
    send_reply( cliSock, 202, ['Command not implemented'] )

  elif cmd == "CCC":	#Clear Command Channel.	RFC 2228
    send_reply( cliSock, 202, ['Command not implemented'] )

  elif cmd == "CDUP":	#Change to parent directory.	RFC 959
    if len( CLIENT_CONTEXT['pwd'] ) > 0:
      CLIENT_CONTEXT['pwd'].pop()
    send_reply( cliSock, 250, ['Directory successfully changed'] )

  elif cmd == "CONF":	#Confidentiality Protected Command.	RFC 2228
    send_reply( cliSock, 202, ['Command not implemented'] )

  elif cmd == "CWD":	#Change working directory.	RFC 697
    curPath = CLIENT_CONTEXT['pwd']

    if args != '':
      if args[0] == os.path.sep:	# absolute path
        curPath = FTPROOT + args.split( os.path.sep )[1:]

      else:			# relative path
        for item in args.split( os.path.sep ):
          if item == '..':
            if len( curPath ) == 0:
              continue

            else:
              curPath.pop()

          elif item == '':
            continue

          else:
            curPath.append( item )

    # check for path really exists and lie in FTPROOT area
    if os.path.isdir( os.path.sep + os.path.sep.join( curPath ) ) and curPath[:len( FTPROOT )] == FTPROOT:
      CLIENT_CONTEXT['pwd'] = curPath
      send_reply( cliSock, 250, ['Directory successfully changed'] )

    else: # error occured
      send_reply( cliSock, 550, ['Permission denied'] )
    del curPath



  elif cmd == "DELE":	#Delete.
    send_reply( cliSock, 202, ['Command not implemented'] )

  elif cmd == "ENC":	#Privacy Protected Command.	RFC 2228
    send_reply( cliSock, 202, ['Command not implemented'] )

  elif cmd == "EPRT":	#Extended Data port.	RFC 2428
    send_reply( cliSock, 202, ['Command not implemented'] )

  elif cmd == "EPSV":	#Extended Passive.	RFC 2428
    send_reply( cliSock, 202, ['Command not implemented'] )

  elif cmd == "FEAT":	#Feature.	RFC 2389
    send_reply( cliSock, 202, ['Command not implemented'] )

  elif cmd == "HELP":	#Help.
    send_reply( cliSock, 214, ['The following commands are recognized', \
 'ABOR', 'ACCT', 'ALLO', 'APPE', 'CDUP', 'CWD', \
 'DELE', 'EPRT', 'EPSV', 'FEAT', 'HELP', 'LIST', 'MDTM', 'MKD', \
 'MODE', 'NLST', 'NOOP', 'OPTS', 'PASS', 'PASV', \
 'PORT', 'PWD' , 'QUIT', 'REIN', 'REST', 'RETR', 'RMD',  'RNFR', \
 'RNTO', 'SITE', 'SIZE', 'SMNT', 'STAT', 'STOR', \
 'STOU', 'STRU', 'SYST', 'TYPE', 'USER', 'XCUP', 'XCWD', 'XMKD', \
 'XPWD', 'XRMD'] )

  elif cmd == "LANG":	#Language negotiation.	RFC 2640
    send_reply( cliSock, 202, ['Command not implemented'] )

  elif cmd == "LIST":	#List.
    """
	FORMAT: drwxr-xr-x   15 ftp      ftp           832 Nov 24 08:40 ubuntu-releases
    """
    if CLIENT_CONTEXT['dataport'] is None:
      send_reply( cliSock, 425, ['Can\'t open data connection'] )

    else:
      send_reply( cliSock, 150, ['Here comes the directory listing'] )
  
      # data = os.popen( 'ls -F %s' % ( os.path.sep + os.path.sep.join( CLIENT_CONTEXT['pwd'] ) ), 'r' )
      data = ''
      # temporary for acceleration of listing...
      path = os.path.sep + os.path.sep.join( CLIENT_CONTEXT['pwd'] )
      try:
        dirlisting = os.listdir( path )

      except:
        send_reply( cliSock, 550, ['Permission denied'] )
        cleanUpDataSock( CLIENT_CONTEXT )

      else:
        for file in os.listdir( path ):
          if file[0] == '.': # do not show hidden files
            continue

          elif os.path.isdir( path + os.path.sep + file ):
            data += file + os.path.sep + '\n'
  
          else:
            data += file + '\n'
  
        CLIENT_CONTEXT['datasock'].send( data )
    
        send_reply( cliSock, 226, ['Directory send OK'] )
        cleanUpDataSock( CLIENT_CONTEXT )
        del data
      del path

  elif cmd == "LPRT":	#Long data port.	RFC 1639
    send_reply( cliSock, 202, ['Command not implemented'] )

  elif cmd == "LPSV":	#Long passive.	RFC 1639
    send_reply( cliSock, 202, ['Command not implemented'] )

  elif cmd == "MDTM":	#File modification time.	RFC 3659
    send_reply( cliSock, 202, ['Command not implemented'] )

  elif cmd == "MIC":	#Integrity Protected Command.	RFC 2228
    send_reply( cliSock, 202, ['Command not implemented'] )

  elif cmd == "MKD":	#Make directory.	RFC 959
    newDir = os.path.sep + os.path.sep.join( CLIENT_CONTEXT['pwd'] ) + os.path.sep + args
    try:
      os.mkdir( newDir )
    except:
      send_reply( cliSock, 550, ['Create directory operation failed'] )
    else:
      # 257 "/pub/test" created
      send_reply( cliSock, 257, ['"%s" created' % ( newDir )] )

  elif cmd == "MLSD":	# 	RFC 3659
    send_reply( cliSock, 202, ['Command not implemented'] )

  elif cmd == "MLST":	# 	RFC 3659
    send_reply( cliSock, 202, ['Command not implemented'] )

  elif cmd == "MODE":	#Transfer mode.
    send_reply( cliSock, 202, ['Command not implemented'] )

  elif cmd == "NLST":	#Name list.
    send_reply( cliSock, 202, ['Command not implemented'] )

  elif cmd == "NOOP":	#No operation.
    send_reply( cliSock, 200, ['NOOP ok'] )

  elif cmd == "OPTS":	#Options.	RFC 2389
    send_reply( cliSock, 200, ['Done'] )

  elif cmd == "PASS":	#Password.
    # CLIENT_CONTEXT['auth'] = True
    send_reply( cliSock, 230, ['Login successful'] )

  elif cmd == "PASV":	#Passive mode.
    sock, CLIENT_CONTEXT['dataport'] = mkDataConn( )
    send_reply( cliSock, 227, \
      ['Entering Passive Mode (%s)' % ( CLIENT_CONTEXT['dataport'] )] )
    CLIENT_CONTEXT['datasock'] =  sock.accept()[0]

  elif cmd == "PBSZ":	#Protection Buffer Size.	RFC 2228
    send_reply( cliSock, 202, ['Command not implemented'] )

  elif cmd == "PORT":	#Data port.
    send_reply( cliSock, 202, ['Command not implemented'] )

  elif cmd == "PROT":	#Data Channel Protection Level.	RFC 2228
    send_reply( cliSock, 202, ['Command not implemented'] )

  elif cmd == "PWD":	#Print working directory.	RFC 959
    send_reply( cliSock, 257, ['"%s"' % ( os.path.sep + os.path.sep.join( \
      CLIENT_CONTEXT['pwd'] ) ) ] )

  elif cmd == "QUIT":	#Logout.
    CLIENT_CONTEXT = {'user':'', 'auth':False, 'pwd':[], 'mode':'A',\
'datasock':None, 'dataport':None}
    send_reply( cliSock, 221, ['bye-bye!'] )

  elif cmd == "REIN":	#Reinitialize.
    send_reply( cliSock, 202, ['Command not implemented'] )

  elif cmd == "REST":	#Restart of interrupted transfer.	RFC 3659
    send_reply( cliSock, 202, ['Command not implemented'] )

  elif cmd == "RETR":	#Retrieve.
    if CLIENT_CONTEXT['dataport'] is None: # exit on no PASV mode data connection
      send_reply( cliSock, 425, ['Can\'t open data connection'] )
      return

    if args[0] == os.path.sep: # absolute path - check for in FTPROOT
      if args.split( os.path.sep )[1:len( FTPROOT )+1] != FTPROOT:
        send_reply( cliSock, 550, ['Permission denied'] )
        return
      else:
        fileName = args

    else: # relative path - add to PWD
      fileName = os.path.sep + os.path.sep.join( CLIENT_CONTEXT['pwd'] ) + os.path.sep + args

    try:
      rc = open( fileName, 'r' )
      rcstat = os.stat( fileName )
    except:
      send_reply( cliSock, 550, ['Permission denied'] )
      cleanUpDataSock( CLIENT_CONTEXT )
      return

    send_reply( cliSock, 150, ['Opening BINARY mode data connection for %s (%d bytes)' % ( args, rcstat[6] )] )

    data = rc.read( 4096 )
    while data:
      cliSock.send( data )
      data = rc.read( 4096 )

    send_reply( cliSock, 226, ['File send OK'] )
    cleanUpDataSock( CLIENT_CONTEXT )
    rc.close( )

  elif cmd == "RMD":	#Remove directory.	RFC 959
    if args.find( '..' ) != -1: # founded backward: deny!
      send_reply( cliSock, 550, ['Permission denied'] )
      return

    rmName = os.path.sep + os.path.sep.join( CLIENT_CONTEXT['pwd'] ) + args
    try:
      os.rmdir( rmName )
    except:
      send_reply( cliSock, 250, ['Requested file action okay, completed'] )
    else:
      send_reply( cliSock, 550, ['Permission denied'] )

  elif cmd == "RNFR":	#Rename from.
    send_reply( cliSock, 202, ['Command not implemented'] )

  elif cmd == "RNTO":	#Rename to.
    send_reply( cliSock, 202, ['Command not implemented'] )

  elif cmd == "SITE":	#Site parameters.
    send_reply( cliSock, 550, ['Permission denied'] )

  elif cmd == "SIZE":	#File size.	RFC 3659
    send_reply( cliSock, 202, ['Command not implemented'] )

  elif cmd == "SMNT":	#Structure mount.	RFC 959
    send_reply( cliSock, 202, ['Command not implemented'] )

  elif cmd == "STAT":	#Status.
    send_reply( cliSock, 202, ['Command not implemented'] )

  elif cmd == "STOR":	#Store.
    send_reply( cliSock, 202, ['Command not implemented'] )

  elif cmd == "STOU":	#Store unique.	RFC 959
    send_reply( cliSock, 202, ['Command not implemented'] )

  elif cmd == "STRU":	#File structure.
    send_reply( cliSock, 202, ['Command not implemented'] )

  elif cmd == "SYST":	#System.	RFC 959
    send_reply( cliSock, 215, ['UNIX Type: L8'] )

  elif cmd == "TYPE":	#Representation type.
    if args == 'A':
      CLIENT_CONTEXT['mode'] = 'A'
      send_reply( cliSock, 200, ['Switching to ASCII mode'] )

    elif args == 'I':
      CLIENT_CONTEXT['mode'] = 'I'
      send_reply( cliSock, 200, ['Switching to Binary mode'] )

    else:
      send_reply( cliSock, 500, ['Unrecognised TYPE command'] )

  elif cmd == "USER":	#User name.
    send_reply( cliSock, 331, ['Please specify the password'] )

  elif cmd == "XCUP":	#Change to the parent of the current working directory.	RFC 775
    send_reply( cliSock, 202, ['Command not implemented'] )

  elif cmd == "XMKD":	#Make a directory.	RFC 775
    send_reply( cliSock, 202, ['Command not implemented'] )

  elif cmd == "XPWD":	#Print the current working directory.	RFC 775
    send_reply( cliSock, 202, ['Command not implemented'] )

  elif cmd == "XRCP":	# 	RFC 743
    send_reply( cliSock, 202, ['Command not implemented'] )

  elif cmd == "XRMD":	#Remove the directory.	RFC 775
    send_reply( cliSock, 202, ['Command not implemented'] )

  elif cmd == "XRSQ":	# 	RFC 743
    send_reply( cliSock, 202, ['Command not implemented'] )

  elif cmd == "XSEM":	#Send, Mail if cannot.	RFC 737
    send_reply( cliSock, 202, ['Command not implemented'] )

  elif cmd == "XSEN":	#Send to terminal.	RFC 737
    send_reply( cliSock, 202, ['Command not implemented'] )

  else: # error
    send_reply( cliSock, 202, ['{%s} Command not implemented' % ( cmd )] )

gS = socket.socket( socket.AF_INET, socket.SOCK_STREAM )
gS.setsockopt( socket.SOL_SOCKET, socket.SO_REUSEADDR, 1 )
gS.bind( ( HOST, PORT ) )
gS.listen( MAX_CLIENT )

try:
  cliSock, cliAddr = gS.accept()
except:
  sys.stderr.write( 'Error while waiting for client...\n' )
  sys.exit( 1 )

sys.stderr.write( 'Connection from %s:%d\n' % cliAddr )

# init new connection
init_new_client( cliSock, cliAddr )
try:
  data = cliSock.recv( 4096 )
except:
  sys.stderr.write( 'SigTERM catched... exit\n' )
  sys.exit( 1 )

while True:
  parse_request( cliSock, \
    data[:4].strip().upper(), \
    stripNL( data[4:].strip() ) )

  try:
    data = cliSock.recv( 4096 )

  except:
    sys.stderr.write( 'SigTERM catched... exit\n' )
    sys.exit( 1 )

cliSock.shutdown( 2 )
cliSock.close()

# gS.shutdown( 2 )
# gS.close()

sys.exit( 0 )
